//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) 2012 James Whitworth
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//----------------------------------------------------------------------------------------------------------------------
#include <gmock/gmock.h>

#include <sqbind/sqbAssert.h>
#include <sqbind/sqbClassTypeTag.h>

#include "fixtures/SquirrelFixture.h"
#include "TypeHelpers.h"
//----------------------------------------------------------------------------------------------------------------------

//----------------------------------------------------------------------------------------------------------------------
typedef SquirrelFixture ClassTypeTagTest;
typedef SquirrelFixture ClassTypeTagDeathTest;

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassTypeTagTest, TestGet)
{
  sqb::ClassTypeTagBase* classTypeTag = sqb::ClassTypeTag<sqb::NoBaseClass>::Get();
  EXPECT_TRUE(classTypeTag == nullptr);
}

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassTypeTagTest, TestIsValid)
{
  sqb::ClassTypeTagBase* validClassTypeTag = sqb::ClassTypeTag<int32_t>::Get();
  EXPECT_TRUE(validClassTypeTag->IsValid());

  int32_t buffer = 0;
  sqb::ClassTypeTagBase* invalidClassTypeTag = reinterpret_cast<sqb::ClassTypeTagBase*>(&buffer);
  EXPECT_FALSE(invalidClassTypeTag->IsValid());
}

//----------------------------------------------------------------------------------------------------------------------
/// ClassTypeTag offset test.
//----------------------------------------------------------------------------------------------------------------------
namespace
{
struct Base { int32_t m_pad[4]; };
struct OtherBase { int32_t m_pad[1]; };
struct DerivedChildVirtual : public Base { virtual ~DerivedChildVirtual() {} };
struct DerivedChildNoVirtual : public Base { };
struct MultiplyDerivedGrandChildVirtual : public OtherBase, public DerivedChildVirtual { };
struct MultiplyDerivedGrandChildNoVirtual : public OtherBase, public DerivedChildNoVirtual { };
}

//----------------------------------------------------------------------------------------------------------------------
template<typename DerivedType, typename BaseType>
void CheckOffsetTo()
{
  // Checks that performing a static_cast from derived to base results in the same pointer value
  // as subtracting the offset from the original derived pointer value.
  //
  const uint8_t * const pointer = reinterpret_cast<const uint8_t*>(0xBADF00D);
  const DerivedType * const derived = reinterpret_cast<const DerivedType *>(pointer);

  const BaseType * const base = static_cast<const BaseType *>(derived);
  ptrdiff_t expected = reinterpret_cast<ptrdiff_t>(base);

  ptrdiff_t offset = sqb::ClassTypeTag<DerivedType>::Get()->GetOffsetTo(sqb::ClassTypeTag<BaseType>::Get());
  ptrdiff_t actual = reinterpret_cast<ptrdiff_t>(pointer - offset);

  EXPECT_EQ(expected, actual);
}

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassTypeTagDeathTest, TestOffset)
{
  // setup the class hierarchies.
  //
  sqb::ClassTypeTag<DerivedChildVirtual>::Get()->SetBaseClass(static_cast<Base *>(nullptr));
  sqb::ClassTypeTag<DerivedChildNoVirtual>::Get()->SetBaseClass(static_cast<Base *>(nullptr));
  sqb::ClassTypeTag<MultiplyDerivedGrandChildVirtual>::Get()->SetBaseClass(static_cast<DerivedChildVirtual *>(nullptr));
  sqb::ClassTypeTag<MultiplyDerivedGrandChildNoVirtual>::Get()->SetBaseClass(static_cast<DerivedChildNoVirtual *>(nullptr));

  // test Base offsets.
  //
  EXPECT_FALSE(sqb::ClassTypeTag<Base>::Get()->MayHaveOffset());
#if SQBIND_ASSERTS_ENABLED != 0
  EXPECT_DEATH(sqb::ClassTypeTag<Base>::Get()->GetOffsetTo(nullptr), "");
  EXPECT_DEATH(sqb::ClassTypeTag<Base>::Get()->GetOffsetTo(sqb::ClassTypeTag<OtherBase>::Get()), "");
#endif
  EXPECT_EQ(0, sqb::ClassTypeTag<Base>::Get()->GetOffsetTo(sqb::ClassTypeTag<Base>::Get()));

  // test DerivedChildVirtual offsets
  //
  EXPECT_TRUE(sqb::ClassTypeTag<DerivedChildVirtual>::Get()->MayHaveOffset());
#if SQBIND_ASSERTS_ENABLED != 0
  EXPECT_DEATH(sqb::ClassTypeTag<Base>::Get()->GetOffsetTo(nullptr), "");
  EXPECT_DEATH(sqb::ClassTypeTag<Base>::Get()->GetOffsetTo(sqb::ClassTypeTag<OtherBase>::Get()), "");
#else
  EXPECT_EQ(0, sqb::ClassTypeTag<Base>::Get()->GetOffsetTo(nullptr));
  EXPECT_EQ(0, sqb::ClassTypeTag<Base>::Get()->GetOffsetTo(sqb::ClassTypeTag<OtherBase>::Get()));
#endif
  EXPECT_EQ(0, sqb::ClassTypeTag<DerivedChildVirtual>::Get()->GetOffsetTo(sqb::ClassTypeTag<DerivedChildVirtual>::Get()));
  CheckOffsetTo<DerivedChildVirtual, Base>();

  // test DerivedChildNoVirtual offsets
  //
  EXPECT_FALSE(sqb::ClassTypeTag<DerivedChildNoVirtual>::Get()->MayHaveOffset());
  CheckOffsetTo<DerivedChildNoVirtual, Base>();

  // test MultiplyDerivedGrandChildVirtual offsets
  //
  EXPECT_TRUE(sqb::ClassTypeTag<MultiplyDerivedGrandChildVirtual>::Get()->MayHaveOffset());
  CheckOffsetTo<MultiplyDerivedGrandChildVirtual, Base>();
  CheckOffsetTo<MultiplyDerivedGrandChildVirtual, DerivedChildVirtual>();

  // test MultiplyDerivedGrandChildNoVirtual offsets
  //
  EXPECT_TRUE(sqb::ClassTypeTag<MultiplyDerivedGrandChildNoVirtual>::Get()->MayHaveOffset());
  CheckOffsetTo<MultiplyDerivedGrandChildNoVirtual, Base>();
  CheckOffsetTo<MultiplyDerivedGrandChildNoVirtual, DerivedChildNoVirtual>();
}

//----------------------------------------------------------------------------------------------------------------------
/// ClassTypeTag name test
//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassTypeTagTest, TestName)
{
  // check the name of the declared class types
  //
  EXPECT_STREQ(_SC("BoundClass"), sqb::ClassTypeTag<BoundClass>::Get()->GetTypeName());

  // check the name of an undeclared class type
  //
  EXPECT_STREQ(_SC(""), sqb::ClassTypeTag<UnboundClass>::Get()->GetTypeName());
}

//----------------------------------------------------------------------------------------------------------------------
/// ClassTypeTag copy functions test.
//----------------------------------------------------------------------------------------------------------------------
namespace
{
struct Copyable { int value; };
struct NonCopyable { int value; };
}

SQBIND_TYPE_NON_COPYABLE(NonCopyable);

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassTypeTagDeathTest, TestNonCopyable)
{
  NonCopyable a, b;

  a.value = 10;
  b.value = 5;

  sqb::CopyInstanceFunction copy = sqb::ClassTypeTag<NonCopyable>::Get()->GetCopyFunction();
#if SQBIND_ASSERTS_ENABLED != 0
  EXPECT_DEATH(copy(&a, &b), "");
#else
  copy(&a, &b);
#endif

  EXPECT_NE(b.value, a.value);
}

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassTypeTagTest, TestCopyable)
{
  Copyable a, b;

  a.value = 10;
  b.value = 5;

  sqb::CopyInstanceFunction copy = sqb::ClassTypeTag<Copyable>::Get()->GetCopyFunction();
  copy(&a, &b);

  EXPECT_EQ(b.value, a.value);
}

//----------------------------------------------------------------------------------------------------------------------
/// ClassTypeTag release hook test.
//----------------------------------------------------------------------------------------------------------------------
SQInteger TestReleaseHook(SQUserPointer SQBIND_UNUSED(pointer), SQInteger SQBIND_UNUSED(size))
{
  return 0;
}

//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassTypeTagTest, TestReleaseHook)
{
  // check the release hook starts as null.
  //
  EXPECT_EQ(nullptr, sqb::ClassTypeTag<BoundClass>::Get()->GetReleaseHook());

  // check setting then getting the release hook returns the same thing.
  //
  sqb::ClassTypeTag<BoundClass>::Get()->SetReleaseHook(&TestReleaseHook);
  EXPECT_EQ(&TestReleaseHook, sqb::ClassTypeTag<BoundClass>::Get()->GetReleaseHook());

  // reset the hook to null
  //
  sqb::ClassTypeTag<BoundClass>::Get()->SetReleaseHook(nullptr);
  EXPECT_EQ(nullptr, sqb::ClassTypeTag<BoundClass>::Get()->GetReleaseHook());
}

//----------------------------------------------------------------------------------------------------------------------
/// ClassTypeTag class object test.
//----------------------------------------------------------------------------------------------------------------------
TEST_F(ClassTypeTagDeathTest, TestClassObject)
{
  // check the class object starts off as null
  //
  HSQOBJECT actual = sqb::ClassTypeTag<BoundClass>::Get()->GetClassObject(m_vm);

  EXPECT_TRUE(sq_isnull(actual));

  sq_resetobject(&actual);

  // create and set the class object
  //
  EXPECT_SQ_SUCCEEDED(m_vm, sq_newclass(m_vm, SQFalse));
  HSQOBJECT expected;
  EXPECT_SQ_SUCCEEDED(m_vm, sq_getstackobj(m_vm, 1, &expected));
  sqb::ClassTypeTag<BoundClass>::Get()->SetClassObject(m_vm, expected);

  // check the class object was set and can be retrieved
  //
  actual = sqb::ClassTypeTag<BoundClass>::Get()->GetClassObject(m_vm);

  EXPECT_FALSE(sq_isnull(actual));
  EXPECT_TRUE(sq_isclass(actual));

  // check the class user data size is 0.
  //
  EXPECT_TRUE(sqb::ClassTypeTag<BoundClass>::Get()->CheckClassUserDataSize(m_vm, 0));

  // set the class user data size and check it
  //
  EXPECT_SQ_SUCCEEDED(m_vm, sq_setclassudsize(m_vm, 1, 10));
  EXPECT_TRUE(sqb::ClassTypeTag<BoundClass>::Get()->CheckClassUserDataSize(m_vm, 10));

  // remove the class from the stack
  //
  sq_poptop(m_vm);

  // check an unbound class type
  //
  EXPECT_TRUE(sqb::ClassTypeTag<UnboundClass>::Get()->CheckClassUserDataSize(m_vm, -1));

  // check the behaviour if the object in the registry table is not a class
  //
#if SQBIND_ASSERTS_ENABLED != 0
  sq_pushregistrytable(m_vm);
  sq_pushuserpointer(m_vm, sqb::ClassTypeTag<UnboundClass>::Get());
  sq_pushinteger(m_vm, 10);
  sq_rawset(m_vm, -3);

  EXPECT_DEATH(sqb::ClassTypeTag<UnboundClass>::Get()->CheckClassUserDataSize(m_vm, -1), "");

  // remove the registry table
  //
  sq_poptop(m_vm);
#endif
}
